;
; ldrdevi.s
; Usage: OS Loader, device identify module
; Create Date: 2008-03-08
; Creator: Frank van Gilluwe from <<Inside PC>>
;
global sysvalue
global pci_detect

[BITS 32]
[SECTION .text]
old_int6_seg	dd	0	       ; temp storage for old int 6
old_int6_off	dd	0              ;  vector (bad opcode)
badoff		dd	0              ; temp return offset if bad offset
			               ;  interrupt 6 called
BaseOfLoader	equ	0x2000
buffer_seg	equ	0x3000

;
;    SYSTEM TYPE DETECTION SUBROUTINE
;       Determine the type of system the software is running
;       on.
;
;       Called with:    nothing
;
;       Returns:        al = System type
;                             0 if PC (8088 based)
;                             1 if XT (8088 based)
;                             2 if PC convertible (8088 based)
;                             3 if PC jr (8088 based)
;                             4 other pre-80286 based machine
;                             8 if XT (80286 based)
;                            10h if AT or ISA
;                            20h if EISA
;                            40h if MCA
;
;       Regs used:      ax, bx
;                       eax, ebx (386 or later)
;
;       Subs called:    cpu386
;
sysvalue:
        push    cx
        push    dx
        push    es

        call    cpu386		   ; get the cpu type in al
        mov     cl, al             ; save cpu class

; Avoid directly reading BIOS ROM, because a few memory managers
; like 386MAX alter bytes at the end of the BIOS.

        push    cx                 ; save cpu number on stack
        mov     ah, 0C0h
        int     15h                ; get BIOS config data es:bx
        pop     cx
        jc      .sys_skp1          ; jump if no config support
                                   ;   (old BIOS)
        mov     dl, [es:bx+2]      ; get model byte
        mov     dh, [es:bx+3]      ; get submodel byte

        mov     al, 40h            ; assume MCA
        test    byte [es:bx+5], 2
        jnz     .sys_Exit           ; exit if MCA
        jmp     .sys_skp2

; we only get here on older PCs in which a memory manager
;  can not be run

.sys_skp1:                          ; ok, get BIOS model directly
        mov     ax, 0F000h
        mov     es, ax             ; point into system BIOS
        mov     dx, [es:0FFFEh]    ; get model & submodel byte

; now use the model and submodel bytes im DX to determine machine

.sys_skp2:
        xor     al, al             ; assume PC (al=0)
        cmp     dl, 0FFh
        je      .sys_Exit           ; jump if PC
        inc     al                 ; assume XT (al=1)
        cmp     dl, 0FEh
        je      .sys_Exit           ; jump if XT
        cmp     dl, 0FBh
        je      .sys_Exit           ; jump if XT
        inc     al                 ; assume PC convertible (al=2)
        cmp     dl, 0F9h
        je      .sys_Exit           ; jump if convertible
        inc     al                 ; assume PCjr (al=3)
        cmp     dl, 0FDh
        je      .sys_Exit           ; jump if PCjr
        inc     al                 ; assume other pre-286 (al=4)
        cmp     cl, 2              ; cl=CPU type - pre-286 ?
        jb      .sys_Exit           ; jump so
        ja      .sys_skp3           ; jump if 386 or above

; possible a 286 XT - use the model and submodel bytes to
;  determine

        mov     al, 8              ; assumption for 286XT
        cmp     dx, 02FCh          ; model code for 286XT ?
        je      .sys_Exit           ; jump if so

; check if EISA system by looking for the "EISA" string at
;  address F000:FFD9

.sys_skp3:
        mov     ax, 0F000h
        mov     es, ax
        mov     al, 10h            ; assume a standard AT/ISA
        cmp     word [es:0FFD9h], 'IE'
        jne     .sys_Exit           ; jump if not EISA
        cmp     word [es:0FFDBh], 'AS'
        jne     .sys_Exit           ; jump if not EISA
        mov     al, 20h            ; EISA machine
.sys_Exit:
        pop     es
        pop     dx
        pop     cx
        ret


;
;    PCI DETECTION SUBROUTINE
;       Determine if the BIOS supports PCI, and if so, get the
;       PCI BIOS version.
;
;       Called with:    nothing
;
;       Returns:        carry = 0 if PCI BIOS present and:
;                         bx = version in BCD
;                       carry = 1 if no PCI BIOS support
;
;       Regs used:      ax, bx
;                       edx (386 or later)
;
;       Subs called:    cpu386
;
pci_detect:
        push    cx

        call    cpu386		       ; get the cpu type in al
        cmp     al, 3              ; 386 or better ?
        jae     .pci_check          ; jump if so
        stc                        ; set carry flag (no PCI)
        jmp     .pci_exit

.pci_check:
        mov     ax, 0B101h         ; function for PCI detect
        mov     edx, " PCI"        ; signifier
        int     1Ah                ; check if present

.pci_exit:
        pop     cx
        ret


;
;    CPU 386 IDENTIFICATION SUBROUTINE
;       Identify the CPU type, from 8088 to 386+.  This is
;       subset of the more extensive CPUVALUE program.  It is
;       used when identification of CPUs above the 386 is
;       not necessary (i.e. 32-bit support or not)
;
;       Called with:    nothing
;
;       Returns:        al = CPU type
;                             0 if 8088/8086 or V20/V30
;                             1 if 80186/80188
;                             2 if 80286
;                             3 if 80386 or better
;
;       Regs used:      ax, bx
;                       eax (32-bit CPU only)
;
;       Subs called:    hook_int6, restore_int6, bad_op_handler
;
cpu386:
        push    cx
        push    dx
        push    ds
        push    es

; 8088/8086 test - Use rotate quirk - All later CPUs mask the CL
;   register with 0Fh, when shifting a byte by cl bits.  This
;   test loads CL with a large value (20h) and shifts the AX
;   register right.  With the 8088, any bits in AX are shifted
;   out, and becomes 0.  On all higher level processors, the
;   CL value of 20h is anded with 0Fh, before the shift.  This
;   means the effective number of shifts is 0, so AX is
;   unaffected.

        mov     cl, 20h            ; load high CL value
        mov     ax, 1              ; load a non-zero value in AX
        shr     ax, cl             ; do the shift
        cmp     ax, 0              ; if zero, then 8088/86
        jne     .up186              ; jump if not 8088/86
        jmp     .up_Exit

; 80186/80188 test - Check what is pushed onto the stack with a
;   PUSH SP instruction.  The 80186 updates the stack pointer
;   before the value of SP is pushed onto the stack.  With all
;   higher level processors, the current value of SP is pushed
;   onto the stack, and then the stack pointer is updated.

.up186:
        mov     bx, sp             ; save the current stack ptr
        push    sp                 ; do test
        pop     ax                 ; get the pushed value
        cmp     ax, bx             ; did SP change ?
        je      .up286              ; if not, it's a 286+
        mov     ax, 1              ; set 80186 flag
        jmp     .up_Exit

; 80286 test A - We'll look at the top four bits of the EFLAGS
;   register.  On a 286, these bits are always zero.  Later
;   CPUs allow these bits to be changed.  During this test,
;   We'll disable interrupts to ensure interrupts do not change
;   the flags.

.up286:
        cli                        ; disable interrupts
        pushf                      ; save the current flags

        pushf                      ; push flags onto stack
        pop     ax                 ; now pop flags from stack
        or      ax, 0F000h         ; try and set bits 12-15 hi
        push    ax
        popf                       ; set new flags
        pushf
        pop     ax                 ; see if upper bits are 0

        popf                       ; restore flags to original
        sti                        ; enable interrupts
        test    ax, 0F000h         ; were any upper bits 1 ?
        jnz     .up386plus          ; if so, not a 286

; 80286 test B - If the system was in V86 mode, (386 or higher)
;   the POPF instruction causes a protection fault, and the
;   protected mode software must emulate the action of POPF. If
;   the protected mode software screws up, as occurs with a
;   rarely encountered bug in Windows 3.1 enhanced mode, the
;   prior test may look like a 286, but it's really a higher
;   level processor. We'll check if the protected mode bit is
;   on.  If not, it's guaranteed to be a 286.

	                               ; allow a 286 instruction
        smsw    ax                 ; get machine status word
        test    ax, 1              ; in protected mode ?
        jz      .is286              ; jump if not (must be 286)

; 80286 test C - It's very likely a 386 or greater, but it is
;   not guaranteed yet.  There is a small possibility the system
;   could be in 286 protected mode so we'll do one last test. We
;   will try out a 386 unique instruction, after vectoring the
;   bad-opcode interrupt vector (int 6) to ourselves.

        call    hook_int6          ; do it!
        mov     dword [badoff], .upbad_op3  ; where to go if bad

        xchg    eax, eax           ; 32 bit nop (bad on 286)

        call    restore_int6       ; restore vector
        jmp     .up386plus          ; only gets here if 386
                                   ;  or greater!

; Interrupt vector 6 (bad opcode) comes here if system is a
;   80286 (assuming the 286 protected mode interrupt 6 handler
;   will execute the bad-opcode interrupt).

.upbad_op3:
        call    restore_int6
.is286:
        mov     ax, 2              ; set 80286 flag
        jmp     .up_Exit

.up386plus:
        mov     ax, 3              ; 32-bit CPU (386 or later)

.up_Exit:
        pop     es
        pop     ds
        pop     dx
        pop     cx
        ret

;
;    HOOK INTERRUPT 6
;       Save the old interrupt 6 vector and replace it with
;       a new vector to the bad_op_handler.  Vectors are handled
;       directly without using DOS.
;
;       Called with:    nothing
;
;       Returns:        vector hooked
;                       old vector stored at
;                         ds:[old_int6_seg]
;                         ds:[old_int6_off]
;
;       Regs used:      none
;
hook_int6:
        push    ax
        push    cx
        push    es
        xor     ax, ax
        mov     es, ax
        cli                        ; disable interrupts
        mov     ax, [es:6*4]       ; get offset of int 6
        mov     cx, [es:6*4+2]     ; get segment
        mov     dword [es:6*4], bad_op_handler
        mov     word [es:6*4+4], BaseOfLoader
        sti                        ; enable interrupts
        mov     [old_int6_seg], cx ; save original vector
        mov     [old_int6_off], ax
        pop     es
        pop     cx
        pop     ax
        ret

;
;    RESTORE INTERRUPT 6
;       Restore the previously saved old interrupt 6 vector.
;       Vectors handled directly without using DOS.
;
;       Called with:    old vector stored at
;                         ds:[old_int6_seg]
;                         ds:[old_int6_off]
;
;       Returns:        vector restored
;
;       Regs used:      none
;
restore_int6:
        push    ax
        push    cx
        push    dx
        mov     cx, [old_int6_seg] ; get original vector
        mov     dx, [old_int6_off]
        push    es
        xor     ax, ax
        mov     es, ax
        cli                        ; disable interrupts
        mov     [es:6*4], dx       ; restore original int 6
        mov     [es:6*4+2], cx
        sti                        ; enable interrupts
        pop     es
        pop     dx
        pop     cx
        pop     ax
        ret

;
;    BAD OFFSET INTERRUPT HANDLER
;       If a bad opcode occurs (80286 or later) will come here.
;       The saved BADOFF offset is used to goto the routine
;       previously stored in BADOFF.
;
;       In a few cases, it is also used for double faults. A few
;       instructions (RDMSR & WRMSR) can issue a double fault if
;       not supported, so well come here as well.
;
;       Called with:    cs:[badoff] previously set
;
;       Returns:        returns to address stored in badoff
;
bad_op_handler:
        push    ax
        push    bp
        mov     ax, [cs:badoff]
        mov     bp, sp
        mov     [ss:bp+4], ax      ; insert new return offset
        pop     bp
        pop     ax
        iret


;
;    DISKETTE DRIVE TYPE DETECTION SUBROUTINE
;       Determine the type of diskette drive
;
;       Called with:    dl = drive to check (0=a:, 1=b:, etc.)
;
;       Returns:        al = drive type
;                               0 = no drive
;                               1 = 360KB
;                               2 = 1.2MB
;                               3 = 720KB
;                               4 = 1.44MB
;                               5 = 2.88MB
;                               6 = 320KB  (obsolete)
;                               7 = unknown type
;
;       Regs used:      ax
;
dsktype:
        push    bx
        push    cx
        push    dx
        push    es

        int     11h                ; equipment check
        test    ax, 1              ; any drives ?
        jz      .dskt_none         ; jump if not
        and     al, 0C0h           ; get # of drives bits
        mov     cl, 2
        rol     al, cl             ; convert to # of drives
        cmp     al, dl             ; is drive available ?
        jb      .dskt_none         ; jump if not

; first we'll try getting the drive parameters from the BIOS

        mov     ah, 8
        int     13h                ; get drive parameters
        jc      .dskt_skp1         ; jump if failed
        mov     al, bl             ; get drive type
        cmp     al, 5              ; unknown type if > 5
        jbe     .dskt_done
        mov     al, 7              ; set to unknown type
        jmp     .dskt_done

; Older systems can't supply drive info, so use diskette table.
;   First the drive is reset.  This forces systems that change
;   the diskette parameter pointer for different drive types
;   to update the pointer to the correct drive type. Interrupt
;   vector 1Eh now points to the correct diskette parameter
;   table.  Only 360KB and 1.2MB drives should get here.

.dskt_skp1:
        xor     ah, ah
        int     13h                ; reset drive
        xor     ax, ax
        mov     es, ax
        mov     bx, [es:1Eh*4]     ; get offset of table
        mov     ax, [es:1Eh*4+2]   ; get segment of table
        mov     es, ax             ; es:bx points to table
        mov     ah, [es:bx+4]      ; get sectors per track
        mov     al, 1              ; assume 360K
        cmp     ah, 9              ; 9 sectors per track ?
        je      .dskt_done         ; jump if so
        inc     al
        cmp     ah, 15             ; 15 sectors per track
        je      .dskt_done          ; jump if so
        mov     al, 6              ; unknown type
        cmp     ah, 8              ; 8 sectors per track
        je      .dskt_done         ; jump if so
        inc     al                 ; al=7, unknown type
        jmp     .dskt_done

.dskt_none:
        xor     al, al
.dskt_done:
        pop     es
        pop     dx
        pop     cx
        pop     bx
        ret


;
;    HARD DISK DRIVE TYPE DETECTION SUBROUTINE
;       Determine if a hard drive is attached, its likely type,
;       and the capacity of that drive.
;
;       Called with:    ds = cs
;                       dl = drive to check, 80h=drive 0, etc.
;
;       Returns:        al = drive type
;                               0 = no drive or no controller
;                               1 = XT type controller
;                               2 = AT or IDE type controller
;                               3 = SCSI type controller
;                               4 = PS/2 ESDI type controller
;                               5 = unknown controller type
;                               6 = IDE controller, LBA active
;                       cx = total number of cylinders (includes
;                              the diagnostic cylinder)
;                       dx = total number of heads (1-256)
;                       ah = number of 512 byte sectors (1-63)
;       Regs used:      ax
;
; local data for routine

hd_parm_seg     dw      0               ; pointer to disk
hd_parm_off     dw      0               ;  parameter table
hd_cylinder     dw      0               ; temp # from table
ESDIbuf			equ		0				; ESDI info buffer

hdsktype:
        push    bx
        push    es
        xor     bp, bp             ; used for temp drive type
        cmp     dl, 80h            ; at least 80h ?
        jb      .hdsk_skp1          ; exit if not

; first we will get the number of drives attached

        push    dx
        mov     dl, 80h            ; ask for drive 0
        mov     ah, 8
        int     13h                ; read disk drive parameters
        pop     ax
        add     dl, 7Fh
        cmp     al, dl             ; drive for this number ?
        jbe     .hdsk_skp2
.hdsk_skp1:
        jmp     .hdsk_none          ; jump if out of range

; now determine the controller/disk type

.hdsk_skp2:
        inc     bp                 ; bp=1 assume XT type drive
        mov     dl, al             ; dl = drive number
        push    dx
        mov     ah, 15h
        int     13h
        pop     dx
        cmp     ah, 1              ; invalid request status ?
        je      .hdsk_skp4          ; if so, XT type & exit
        inc     bp                 ; set type to AT
        cmp     ah, 3              ; confirm valid hard disk
        je      .hdsk_skp3
        jmp     .hdsk_none          ; exit if not

; let's check if it's a drive which does not use the drive
;   parameter table like SCSI and some ESDI drives.

.hdsk_skp3:
        inc     bp                 ; assume SCSI
        cmp     dl, 87h            ; if above 87h, assume SCSI
        jbe     .hdsk_skp5
.hdsk_skp4:
        jmp     .hdsk_info          ; jump if so

.hdsk_skp5:
        mov     bx, 4*41h          ; assume ptr to vector 41h
        cmp     dl, 80h            ; drive 0 ?
        je      .hdsk_skp6          ; jump if so
        mov     bx, 4*46h          ; pointer to vector 46h
.hdsk_skp6:
        xor     ax, ax
        mov     es, ax
        mov     si, [es:bx]        ; offset of parameter table
        mov     ax, [es:bx+2]      ; segment of parameter table
        mov     [hd_parm_seg], ax  ; save pointer
        mov     [hd_parm_off], si
        mov     es, ax             ; es:si ptr to table

; we now have a pointer to the disk parameter table, but there
;   are two types!  Check if string at offset 2 is not 'IBM ', then
;   use normal table (cylinder at offset 0).  Otherwise use
;   old PS/2 style, where cylinder is at offset 19h.

        cmp     word [es:si+2], 'BI' ; string 'IBM ' ?
        jne     .hdsk_skp7          ; jump if not
        cmp     word [es:si+4], ' M'
        jne     .hdsk_skp7
        add     si, 19h            ; adjust to PS/2 style
.hdsk_skp7:
        mov     ax, [es:si]        ; get cylinders from table
        mov     [hd_cylinder], ax  ; save
        cmp     ax, 0              ; invalid # of cylinders ?
        jne     .hdsk_skp8          ; jump if ok (non-zero)
        jmp     .hdsk_info          ; we will assume SCSI

.hdsk_skp8:
        dec     bp                 ; assume AT type (2)
        push    dx
        mov     ah, 8
        int     13h                ; get disk parameters
        call    hdconvert          ; convert into useful values
        cmp     ax, 1021           ; near end of IDE limit?
        jb      .hdsk_skp8a         ; jump if not
        cmp     word [hd_cylinder], 1020
        jae     .hdsk_skp8b         ; jump if ok
        jmp     .hdsk_skp9          ; unlikely AT or IDE type

.hdsk_skp8a:
        cmp     ax, [hd_cylinder]  ; are they within 3 cylinders?
        ja      .hdsk_skp9          ; jump if not
        add     ax, 3
        cmp     ax, [hd_cylinder]  ; are they within 3 cylinders?
        jb      .hdsk_skp9          ; jump if not

        ; is AT type, check if LBA active (heads > 16)

.hdsk_skp8b:
        cmp     dx, 16             ; more than 16 heads ?
        jbe     .hdsk_info2         ; jump if not
        mov     bp, 6              ; set to AT type with LBA on (6)
.hdsk_info2:
        pop     dx
        jmp     .hdsk_info          ; if so, likely AT type

; Not likely AT type, since cylinders do not match up!
;  First we'll try an older Future Domain SCSI test

; Future Domain SCSI test - Interrupt 13h, function 18h will
;  return invalid command with DL >= 80h, if no Future Domain
;  SCSI card is present

.hdsk_skp9:
        pop     dx
        inc     bp                 ; assume SCSI
        push    dx
        mov     ah, 18h
        int     13h                ; Future Domain SCSI ?
        pop     dx
        jc      .hdsk_skp10         ; jump if not
        cmp     ax, 4321h          ; confirmation number
        jne     .hdsk_skp10
        jmp     .hdsk_info

; Now check if possible PS/2 ESDI drive

.hdsk_skp10:
        mov		ax, buffer_seg
		mov		es, ax
        push    dx
        mov     ax, 1B0Ah
        mov     bx, ESDIbuf
        int     13h                ; Get ESDI config
        pop     dx
        jc      .hdsk_skp11         ; jump if not PS/2 ESDI
        cmp     bx, ESDIbuf
        jne     .hdsk_skp11
        mov     ax, cs
        mov     bx, es
        cmp     ax, bx             ; is CS = ES ?
        jne     .hdsk_skp11         ; jump if not
        inc     bp
        jmp     .hdsk_info

; General SCSI test (not drive specific)

.hdsk_skp11:
        xor     ax, ax
        mov     es, ax
        cmp     word [es:4*4Fh+2], 0  ; vector valid ?
        je      .hdsk_skp12         ; jump if not
        push    dx
        mov     ax, 8200h
        mov     cx, 8765h
        mov     dx, 0CBA9h
        int     4Fh                ; check for SCSI CAM
        cmp     dx, 5678h
        pop     dx
        jne     .hdsk_skp12         ; jump if not
        cmp     ah, 0
        jne     .hdsk_skp12         ; jump if not
        cmp     cx, 9ABCh
        jne     .hdsk_skp12         ; jump if not
        jmp     .hdsk_info

.hdsk_skp12:
        mov     bp, 5              ; Indicate unknown
                                   ;  controller type

; now get the disk parameter for the specified drive

.hdsk_info:
        mov     ah, 8
        int     13h                ; read disk drive parameters
        call    hdconvert          ; convert to useful value
        xchg    cx, ax             ; put in proper registers
        mov     bl, al             ; sectors
        mov     ax, bp             ; get drive type
        mov     ah, bl             ; insert sectors
        jmp     .hdsk_exit

.hdsk_none:
        xor     al, al             ; no drive with this number
        xor     cx, cx
        xor     dx, dx
.hdsk_exit:
        pop     es
        pop     bx
        ret


;
;    HARD DISK CONVERT
;
;       The cylinder number is in cx.  This routine
;       properly combines them back into a 10 bit
;       cylinder number in AX. The number is increased by
;       one for the diagnostic cylinder, and by one more, since
;       the number is zero based. The upper two bits in CL are
;       cleared to get the sector number.  One is added
;       to the head number, since it is zero based.
;
;       Called with:    ch = lower 8 bits of cylinder number
;                       cl upper two bits are
;                             bits 9 & 8 of cylinder
;                          lower six bits is sector number
;                       dh head number
;
;       Returns:        ax = total combined cylinders
;                       cl = sector number
;                       dx = total heads (1-256)
;
;       Regs used:      cx, dh
;
hdconvert:
        mov     ax, cx             ; get fragmented cylinder #
        rol     al, 1
        rol     al, 1
        and     al, 3
        xchg    al, ah             ; ax = # of cylinders
        and     cx, 3Fh            ; mask for sector number
        mov     dl, dh
        xor     dh,dh
        inc     dx                 ; adjust for head count
        ret

